package org.jmap.core;

import java.io.Serializable;

import org.jmap.geom.AbstractGeometry;
import org.osgeo.proj4j.CoordinateReferenceSystem;
import org.osgeo.proj4j.CoordinateTransform;
import org.osgeo.proj4j.CoordinateTransformFactory;
import org.osgeo.proj4j.ProjCoordinate;

/**
 * encapsulates a rectangular region in 2D Cartesian coordinates defined by 
 * left, right, top and bottom values that correspond to 
 * xmin, xmax, ymin and ymax respectively
 * 
 * <p>Extent objects are not bound to any geometric interface or representation or 
 * measurement system, Extent objects use double precision and can represent any region
 * that has area in 2-dimensional cartesian space
 * </p>
 * 
 * <p>when setting the <code>Extent</code> values, for the extent to be valid:
 * <ul>
 * <li>the left value must be <= right</li>
 * <li>the top value must be <= bottom</li>
 * </ul>
 * 
 * 
 * @author Ali
 *
 */
public class Extent implements Serializable, Cloneable {
	
	private static final long serialVersionUID = 1L;

	public double left;
	public double top;
	public double right;
	public double bottom;
	
	private MapPoint center;
	
	public Extent() {
		//empty stub
	}
	
	public Extent(double left, double bottom, double right, double top) {
		this.left = left;
		this.right = right;
		this.bottom = bottom;
		this.top = top;
	}
	
	/**
	 * an Empty <code>Extent</code> has no dimensions or
	 * has dimensions equal to 0
	 * 
	 * @return boolean true if the dimensions (width and height) are equal to zero
	 * false otherwise
	 */
	public boolean isEmpty() {
		return (getWidth() == 0 || getHeight() == 0);
	}
	
	/** 
	 * gets the width of this extent
	 * 
	 * @return the width of this <code>Extent</code> object
	 */
	public double getWidth() {
		return (this.right - this.left);
	}
	
	/** 
	 * gets the height of this extent
	 * 
	 * @return the height of this <code>Extent</code> object
	 */
	public double getHeight() {
		return (this.top - this.bottom);
	}
	
	/**
	 * gets the <code>Size</code> of this <code>Extent</code> instance
	 * 
	 * @return a <code>Size</code> object with the width and height of this Extent
	 */
	public Dimension getSize() {
		return new Dimension((this.left + this.right) * 0.5, (this.top + this.bottom) * 0.5);
	}
	
	/**
	 * gets the center <code>MapPoint</code> for this extent,
	 * this value is not cached upon class creation and is created dynamically
	 * 
	 * @return the center location as a <code>MapPoint</code>
	 */
	public MapPoint getCenter() {
		return new MapPoint((this.left + this.right) * 0.5, (this.top + this.bottom) * 0.5);
	}

	
	/**
	 * adds arbitrary x and y values and returns a new modified extent, 
	 * this has the effect of shifting the extent in both axes
	 * 
	 * @param x the x value to add to this extent
	 * @param y the y value to ad to this extent
	 * 
	 * @return Extent the newly modified Extent
	 */
	public Extent transform(double x, double y) {
		return new Extent(this.left + x, this.bottom + y, this.right + x, this.top + y);
	}
	
	/**
	 * applies a uniform scale to this <code>Extent</code> considering
	 * the new <code>MapPoint</code> origin
	 * 
	 * @param ratio a double of the ratio of the scale factor
	 * @param origin the origin point to scale to
	 * 
	 * @return the newly scaled extent centered at the Origin point
	 */
	public Extent scale(double ratio, MapPoint origin) {
		if (origin == null) {
			origin = getCenter();
		}
		
		double origx = origin.lon;
		double origy = origin.lat;
		
		double left = (this.left - origx) * ratio + origx;
		double right = (this.right - origx) * ratio + origx;
		double top = (this.top - origy) * ratio + origy;
		double bottom = (this.bottom - origy) * ratio + origy;
		
		return new Extent(left, bottom, right, top);
	}
	
	/**
	 * translates this <code>Extent</code> by arbitrary x and y values
	 * 
	 * @param x the number of units to move this extent to in the x axis
	 * @param y the number of units to move this extent to in the y axis
	 * 
	 * @return the newly modified extent
	 */
	public Extent translate(double x, double y) {
		return new Extent(this.left + x, this.bottom + y, this.right + x, this.top + y);
	}
	
	/**
	 * performs a point in rectangle check
	 * 
	 * @param point the <code>MapPoint</code> to test containment against
	 * @param inclusive true to include borders, false otherwise
	 * @return boolean wether the test MapPoint is in this extent inclusively or not
	 */
	public boolean contains(MapPoint point, boolean inclusive) {
		boolean contains = false;
		
		if (inclusive) {
            contains = ((point.lon >= this.left) && (point.lon <= this.right) && 
                    (point.lat >= this.bottom) && (point.lat <= this.top));
		} else {
            contains = ((point.lon > this.left) && (point.lon < this.right) && 
                    (point.lat > this.bottom) && (point.lat < this.top));
		}
		
		return contains;
	}
	
	/**
	 * Tests intersection with any other Extent
	 * 
	 * @param extent the target extent to test against
	 * @param inclusive wether to include the borders in the test
	 * @return boolean true if the Extents intersect, false otherwise
	 */
	public boolean intersects(Extent extent, boolean inclusive) {

        boolean intersects = false;
        boolean mightTouch = (
            this.left == extent.right ||
            this.right == extent.left ||
            this.top == extent.bottom ||
            this.bottom == extent.top
        );
        
        // if the two extents only touch at an border, and inclusive is false,
        // then the bounds don't intersect.
        if (inclusive || !mightTouch) {
            // otherwise, if one of the extents even partially contain another,
            // inclusive of the border, then they do intersect.
            boolean inBottom = (
                ((extent.bottom >= this.bottom) && (extent.bottom <= this.top)) ||
                ((this.bottom >= extent.bottom) && (this.bottom <= extent.top))
            );
            boolean inTop = (
                ((extent.top >= this.bottom) && (extent.top <= this.top)) ||
                ((this.top > extent.bottom) && (this.top < extent.top))
            );
            boolean inLeft = (
                ((extent.left >= this.left) && (extent.left <= this.right)) ||
                ((this.left >= extent.left) && (this.left <= extent.right))
            );
            boolean inRight = (
                ((extent.right >= this.left) && (extent.right <= this.right)) ||
                ((this.right >= extent.left) && (this.right <= extent.right))
            );
            intersects = ((inBottom || inTop) && (inLeft || inRight));
        }
        
        return intersects;
	}
	
	public boolean intersects(Extent extent, boolean inclusive, Extent worldExtent) {
		
		Extent self = this.wrapDateLine(worldExtent, 0.0d, 0.0d);
		extent = extent.wrapDateLine(worldExtent, 0.0d, 0.0d);
		
        boolean intersects = false;
        boolean mightTouch = (
            self.left == extent.right ||
            self.right == extent.left ||
            self.top == extent.bottom ||
            self.bottom == extent.top
        );
        
        // if the two extents only touch at an edge, and inclusive is false,
        // then the extents don't intersect.
        if (inclusive || !mightTouch) {
            // otherwise, if one of the extents even partially contain each other,
            // inclusive of the borders, then they do intersect.
            boolean inBottom = (
                ((extent.bottom >= self.bottom) && (extent.bottom <= self.top)) ||
                ((self.bottom >= extent.bottom) && (self.bottom <= extent.top))
            );
            boolean inTop = (
                ((extent.top >= self.bottom) && (extent.top <= self.top)) ||
                ((self.top > extent.bottom) && (self.top < extent.top))
            );
            boolean inLeft = (
                ((extent.left >= self.left) && (extent.left <= self.right)) ||
                ((self.left >= extent.left) && (self.left <= extent.right))
            );
            boolean inRight = (
                ((extent.right >= self.left) && (extent.right <= self.right)) ||
                ((self.right >= extent.left) && (self.right <= extent.right))
            );
            
            intersects = ((inBottom || inTop) && (inLeft || inRight));
        }
        
        if (worldExtent != null && !intersects) {
            Extent world = worldExtent;
            double width = world.getWidth();
            boolean selfCrosses = !world.contains(self, false, true);
            boolean boundsCrosses = !world.contains(extent, false, true);
            if (selfCrosses && !boundsCrosses) {
                extent = extent.transform(-width, 0);
                intersects = self.intersects(extent, inclusive);
            } else if (boundsCrosses && !selfCrosses) {
                self = self.transform(-width, 0);
                intersects = extent.intersects(self, inclusive);                
            }
        }
        
        return intersects;
	}
	
    /** 
    * Determine whether the target Extent is contained within this Extent.
    * 
    * 
    * @param Extent - The target Extent object.
    * @param partial - If any of the target corners is within this bounds
    *     consider the bounds contained.  Default is false.  If false, the
    *     entire target bounds must be contained within this bounds.
    * @param inclusive - Treat shared edges as contained.  Default is
    *     true.
    *
    * @Return
    * {Boolean} The passed-in bounds object is contained within this bounds.
    * **/ 
	public boolean contains(Extent extent, boolean partial, boolean inclusive) {
        boolean bottomLeft  = this.contains(new MapPoint(extent.left, extent.bottom), inclusive);
        boolean bottomRight = this.contains(new MapPoint(extent.right, extent.bottom), inclusive);
        boolean topLeft  = this.contains(new MapPoint(extent.left, extent.top), inclusive);
        boolean topRight = this.contains(new MapPoint(extent.right, extent.top), inclusive);

        return (partial) ? (bottomLeft || bottomRight || topLeft || topRight)
                         : (bottomLeft && bottomRight && topLeft && topRight);
	}
	
	public String pointInQuadrant(MapPoint point) {
        String quadrant = "";
        MapPoint center = this.getCenter();
        
        quadrant += (point.lat < center.lat) ? "b" : "t";
        quadrant += (point.lon < center.lon) ? "l" : "r";
        
        return quadrant;
	}
	
	public void extend(Extent extent) {
		if (extent != null) {
			this.center = null;
            if (extent.left < this.left) {
                this.left = extent.left;
            }
            if (extent.bottom < this.bottom) {
                this.bottom = extent.bottom;
            } 
            if (extent.right > this.right) {
                this.right = extent.right;
            }
            if (extent.top > this.top) { 
                this.top = extent.top;
            }
		}
	}

	public void extend(MapPoint point) {
		Extent extent = new Extent(point.lon, point.lat, point.lon, point.lat);
		
		if (extent != null) {
			this.center = null;
            if (extent.left < this.left) {
                this.left = extent.left;
            }
            if (extent.bottom < this.bottom) {
                this.bottom = extent.bottom;
            } 
            if (extent.right > this.right) {
                this.right = extent.right;
            }
            if (extent.top > this.top) { 
                this.top = extent.top;
            }
		}
	}
	
	/**
	 * wraps this Extent around the date line if applicable by shifting it
	 * left or right against the maxExtent supplied
	 * 
	 * @param maxExtent the maximum extent to wrap around
	 * @param leftTolerance tolerance value before the wrapping
	 * @param rightTolerance tolerance value after the wrapping
	 * 
	 * @return the newly wrapped around the date line Extent
	 */
	public Extent wrapDateLine(Extent maxExtent, double leftTolerance, double rightTolerance) {
		Extent newExtent = this.clone();
		
		if (maxExtent != null) {
			double width = maxExtent.getWidth();
			
			//shift right
	           while (newExtent.left < maxExtent.left && 
	                newExtent.right - rightTolerance <= maxExtent.left ) { 
	                newExtent = newExtent.transform(width, 0);
	            }

	            //shift left
	            while (newExtent.left + leftTolerance >= maxExtent.right && 
	                   newExtent.right > maxExtent.right ) { 
	                newExtent = newExtent.transform(-width, 0);
	            }
	           
	            // crosses right only, force left
	            double newLeft = newExtent.left + leftTolerance;
	            if (newLeft < maxExtent.right && newLeft > maxExtent.left && 
	                   newExtent.right - rightTolerance > maxExtent.right) {
	                newExtent = newExtent.transform(-width, 0);
	            }
		}
		
		return newExtent;
	}
	
	
	
	@Override
	public String toString() {
		return String.format(
				"Extent [left=%s, top=%s, right=%s, bottom=%s, center=%s]",
				left, top, right, bottom, this.getCenter().toShortString());
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		long temp;
		temp = Double.doubleToLongBits(bottom);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		result = prime * result + ((center == null) ? 0 : center.hashCode());
		temp = Double.doubleToLongBits(left);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		temp = Double.doubleToLongBits(right);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		temp = Double.doubleToLongBits(top);
		result = prime * result + (int) (temp ^ (temp >>> 32));
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof Extent)) {
			return false;			
		}
		
		Extent other = (Extent) obj;
		if (this.left == other.left && this.right == other.right 
				&& this.top == other.top && this.bottom == other.bottom) {
			return true;
		} else {
			return false;
		}
	}

	public Extent clone() {
	    try {
	        Extent clone = (Extent) super.clone();
	        
	        return clone;
	      }
	      catch (CloneNotSupportedException e) {
	        return null;
	      }
	}
	
	/**
	 * transforms this <code>Extent</code> from a source CoordinateReferenceSystem to a
	 * destination CoordinateReferenceSystem, this operation applies a Projection Transformation
	 * to this Extent by projecting its corner Points
	 * 
	 * @param sourceCRS the source <code>CoordinateReferenceSystem</code> to Project from
	 * @param targetCRS the target <code>CoordinateReferenceSystem</code> to Project to
	 * @return Projected Extent from the <code>surceCRS</code> to the <code>destinationCRS</code>
	 */
	public Extent transform(CoordinateReferenceSystem sourceCRS, CoordinateReferenceSystem targetCRS) {
		//clear center point
		this.center = null;
		
		CoordinateTransformFactory transformFactory = new CoordinateTransformFactory();
		CoordinateTransform transform = transformFactory.createTransform(sourceCRS, targetCRS);
		
		ProjCoordinate ll = new ProjCoordinate();
		ProjCoordinate lr = new ProjCoordinate();
		ProjCoordinate ul = new ProjCoordinate();
		ProjCoordinate ur = new ProjCoordinate();
		
		ll = transform.transform(new ProjCoordinate(this.left, this.bottom), ll);
		lr = transform.transform(new ProjCoordinate(this.right, this.bottom), lr);
		ul = transform.transform(new ProjCoordinate(this.left, this.top), ul);
		ur = transform.transform(new ProjCoordinate(this.right, this.top), ur);
		
        this.left   = Math.min(ll.x, ul.x);
        this.bottom = Math.min(ll.y, lr.y);
        this.right  = Math.max(lr.x, ur.x);
        this.top    = Math.max(ul.y, ur.y);
        
        return this;
	}
}
